﻿namespace LaoLuo
{
    using System;
    using System.Threading;

    /// <summary>
    /// Provides static methods to call an action with a lock
    /// </summary>
    public struct TimedLock : IDisposable
    {
        /// <summary>
        /// default timeout
        /// </summary>
        public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10);

        /// <summary>
        /// object as a lock
        /// </summary>
        private readonly object _obj;

        /// <summary>
        /// timeout of entering the lock
        /// </summary>
        private readonly TimeSpan _timeout;

        /// <summary>
        /// whether throw an exception or not when timeout
        /// </summary>
        private readonly bool _throwTimeoutException;

        /// <summary>
        /// whether lock is entered or not
        /// </summary>
        private bool _isEntered;

        /// <summary>
        /// init a new <see cref="TimedLock"/>
        /// </summary>
        /// <param name="lockObject">object as a lock</param>
        /// <param name="timeout">timeout of entering the lock</param>
        /// <param name="throwTimeoutException">whether throw an exception or not when timeout</param>
        private TimedLock(object lockObject, TimeSpan? timeout, bool throwTimeoutException = false)
        {
            this._throwTimeoutException = throwTimeoutException;
            this._obj = lockObject;
            this._timeout = timeout ?? DefaultTimeout;
            this._isEntered = false;
#if DEBUG
            this._leakDetector = new LeakDetector();
#endif
        }

        /// <summary>
        /// Tries to call an action with a lock in specified time. If timeout, action will NOT be performed.
        /// <para>WARNING: If the action MUST be performed, USE <see cref="TimedLock.Call"/> instead</para>
        /// </summary>
        /// <param name="action">action to call</param>
        /// <param name="lockObject">object as a lock</param>
        /// <param name="timeout">timeout of entering the lock</param>
        /// <returns>action is performed or not</returns>
        public static bool TryCall(Action action, object lockObject, TimeSpan? timeout = null)
        {
            return _TryCall(action, lockObject, timeout, false);
        }

        /// <summary>
        /// Calls an action with a lock in specified time. If timeout, action will NOT be performed, an <see cref="LockTimeoutException"/> will be thrown
        /// </summary>
        /// <param name="action">action to call</param>
        /// <param name="lockObject">object as a lock</param>
        /// <param name="timeout">timeout of entering the lock</param>
        /// <exception cref="LockTimeoutException"/>
        public static void Call(Action action, object lockObject, TimeSpan? timeout = null)
        {
            _TryCall(action, lockObject, timeout, true);
        }

        /// <summary>
        /// dispose
        /// </summary>
        public void Dispose()
        {
            if (this._isEntered)
            {
                Monitor.Exit(this._obj);
            }

#if DEBUG
            GC.SuppressFinalize(this._leakDetector);
#endif
        }

        private static bool _TryCall(Action action, object lockObject, TimeSpan? timeout, bool throwTimeoutException)
        {
            using (var l = new TimedLock(lockObject, timeout, throwTimeoutException))
            {
                if (!l.Lock())
                {
                    return false;
                }

                action();

                return true;
            }
        }

        /// <summary>
        /// Tries to enter the lock
        /// </summary>
        /// <returns>Lock is entered or not</returns>
        /// <exception cref="LockTimeoutException"></exception>
        private bool Lock()
        {
            if (!Monitor.TryEnter(this._obj, this._timeout))
            {
#if DEBUG
                GC.SuppressFinalize(this._leakDetector);
#endif
                if (_throwTimeoutException)
                {
                    throw new LockTimeoutException();
                }

                return false;
            }
            else
            {
                return this._isEntered = true;
            }
        }

#if DEBUG
        private LeakDetector _leakDetector;

        private class LeakDetector
        {
            ~LeakDetector()
            {
                System.Diagnostics.Debug.Fail("Lock is not disposed", "Code 'using (TimedLock.Lock(lockTarget)){}'");
            }
        }
#endif

        /// <summary>
        /// A type of exception should be used when fail to enter a lock in a specified time
        /// </summary>
        public class LockTimeoutException : ApplicationException
        {
            /// <summary>
            /// init a new <see cref="LockTimeoutException"/>
            /// </summary>
            public LockTimeoutException()
                : base("Timeout waiting for lock")
            {
            }
        }
    }
}
